Skip to main content

04 · Reflection:让 Agent 自己验收自己的活

零、开始之前

这篇的目标:能搭出一个「生成 → 评审 → 返工」的循环,并且知道什么情况下这个循环是在白烧钱

需要的前置知识:读过 01 ReAct。本篇的代码不需要工具调用,比前几篇简单。

读完你会明白

  • 为什么把一次模型调用拆成「生成」和「批评」两个角色,质量会提升
  • 为什么大部分人做的 Reflection 其实在空转,以及怎么判断自己是不是
  • 「代码 Agent 到底是 Reflection 还是强化学习」这个常见困惑的答案

这篇有一个前置警告,先说在前面

前三个范式都是「怎么把事做完」。这一篇是「做完的东西够不够好」。 但它是本专题最容易做错的一篇 —— 做错的表现是:流程跑得很漂亮,质量一点没提升。

一、先看一个真实问题

让模型写一段代码:

「实现一个栈,支持 push、pop、getMin,三个操作都要 O(1)。」

模型第一版大概会给你这个:

class MinStack:
def __init__(self):
self.stack = []

def push(self, x):
self.stack.append(x)

def pop(self):
return self.stack.pop()

def getMin(self):
return min(self.stack) # ← 这里是 O(n),不是 O(1)

代码能跑,测试能过,但它没满足要求。 min() 要遍历整个栈。

这类错误有个共同特点,值得注意:

特点说明
工具不报错语法对、能运行、单测能过
ReAct 修不了ReAct 靠工具返回的错误来纠正,这里没有错误
要「看一眼」才发现需要有人对着需求检查一遍

Reflection 就是把这个「看一眼」的动作,也交给模型来做。

二、朴素的办法为什么不够

第一个想法:在 prompt 里强调要求。

prompt = "实现 MinStack,三个操作都必须 O(1)。注意:不要用 min() 遍历!"

这次可能对了。但下一个任务你又得预判它会犯什么错 —— 你在替模型做检查,只是把检查提前到了 prompt 里。

第二个想法:让模型自己检查。

result = llm_call("实现 MinStack,三个操作 O(1)")
check = llm_call(f"检查这段代码有没有问题:{result}")

方向对了。但这里有个关键问题,很多人第一次做都会踩:

如果你在同一次调用里让模型「写完再自己检查一遍」,效果会明显差于拆成两次调用。

为什么?因为「生成」和「批判」是两种不同的任务:

  • 生成时,模型的目标是产出一个完整、自洽的东西
  • 批判时,模型的目标是找出不完整、不自洽的地方

这两个目标是对立的。混在一个 prompt 里,模型会倾向于维护自己刚写的东西 —— 它已经"承诺"了这个方案,很难再推翻。

所以 Reflection 的核心不是「模型会反省」,而是「把一次调用拆成两个互相独立的角色」。

三、概念:三步循环

Reflection 机制中的执行-反思-优化迭代循环

图 4-1 「执行 → 反思 → 优化」循环

图片来源:Datawhale Hello-Agents 第四章(CC BY-NC-SA 4.0)

用大白话说:

写成公式(OiO_i 是第 ii 版产出,FiF_i 是反馈):

Fi=πreflect(Task, Oi)Oi+1=πrefine(Task, Oi, Fi)F_i = \pi_{\text{reflect}}(\text{Task},\ O_i) \qquad O_{i+1} = \pi_{\text{refine}}(\text{Task},\ O_i,\ F_i)

注意「模型实例 A」和「模型实例 B」可以是同一个模型,只是换了一套提示词。区别在于人格,不在于权重。

Evaluator-optimizer workflow

图 4-2 Anthropic 对同一模式的命名:evaluator-optimizer(评审-优化)

图片来源:Anthropic — Building Effective Agents

3.1 它和前三个范式的关系

关键差别在**「纠错信号从哪来」**:

范式纠错信号来源
ReAct外部:工具返回的报错告诉它错了
Workflow没有纠错,路径写死
Plan-and-Execute计划层:执行不下去就重新规划
Reflection内部:没有天然的外部信号,靠另一个模型实例挑刺

最后那个「内部」三个字,是这一篇所有麻烦的来源。第六节会专门讲。

四、动手:三个函数搭出来

拆解对象是 claude-cookbookspatterns/agents/evaluator_optimizer.ipynb(MIT)。整个实现三个函数、六十行。

4.1 第一步:生成器

def generate(prompt: str, task: str, context: str = "") -> tuple[str, str]:
full_prompt = f"{prompt}\n{context}\nTask: {task}" if context else f"{prompt}\nTask: {task}"
response = llm_call(full_prompt)
thoughts = extract_xml(response, "thoughts")
result = extract_xml(response, "response")
return thoughts, result

配套的生成器提示词:

Your goal is to complete the task based on <user input>. If there are feedback
from your previous generations, you should reflect on them to improve your solution

<thoughts>
[Your understanding of the task and feedback and how you plan to improve]
</thoughts>

<response>
[Your code implementation here]
</response>

两个设计点

<thoughts> 必须排在 <response> 前面。

02 路由 里的道理完全一样:自回归模型先写出的「我打算怎么改」,会真实约束后面生成的代码。顺序反过来,就退化成给已经写完的代码编理由。

context 参数是可选的,这让一个函数干了两件事。

  • 第一轮:没有 context,走 else 分支 → 这是初稿
  • 后续轮:有 context(包含反馈)→ 这是返工

为什么不拆成两个函数?因为这样能保证两轮用的是完全相同的生成器人格。如果初稿和返工用不同的 prompt,返工时模型的风格会漂。

4.2 第二步:评审员

def evaluate(prompt: str, content: str, task: str) -> tuple[str, str]:
full_prompt = f"{prompt}\nOriginal task: {task}\nContent to evaluate: {content}"
response = llm_call(full_prompt)
evaluation = extract_xml(response, "evaluation")
feedback = extract_xml(response, "feedback")
return evaluation, feedback

评审提示词里有一句是全篇最重要的:

Evaluate this following code implementation for:
1. code correctness
2. time complexity
3. style and best practices

You should be evaluating only and not attemping to solve the task.
Only output "PASS" if all criteria are met and you have no further suggestions.

<evaluation>PASS, NEEDS_IMPROVEMENT, or FAIL</evaluation>
<feedback>
What needs improvement and why.
</feedback>

(原文那个 attemping 拼写错误是官方仓库里就有的,不是抄错了。)

「evaluating only, not attempting to solve」这一句必须有。

不加它会发生什么?评审员会自己动手把代码重写一遍。 于是你拿到的不是反馈,而是另一份实现 —— 生成器完全被架空,两个角色塌缩成了一个。

还有一个容易忽略的细节:评审员拿到的是 contenttask,没有拿到生成器的 <thoughts>

这也是刻意的。评审应该只看产物,不看作者的自辩。让评审员看到「我本来是这么想的,因为 XX 原因所以这么写」,它很容易被说服。

4.3 第三步:主循环

def loop(task: str, evaluator_prompt: str, generator_prompt: str):
memory = []

thoughts, result = generate(generator_prompt, task) # 初稿
memory.append(result)

while True:
evaluation, feedback = evaluate(evaluator_prompt, result, task)
if evaluation == "PASS":
return result

context = "\n".join([
"Previous attempts:",
*[f"- {m}" for m in memory],
f"\nFeedback: {feedback}",
])
thoughts, result = generate(generator_prompt, task, context) # 返工
memory.append(result)

跑一遍会看到这样的输出:

第 1 版:getMin 用了 min(self.stack)
评审:NEEDS_IMPROVEMENT — getMin 是 O(n),建议维护一个辅助的最小值栈
第 2 版:加了 self.min_stack,push 时同步维护
评审:PASS ✅

五、这段官方示例代码里的三个坑

示例代码为了简洁省掉了很多东西,照抄上生产会出事

5.1 坑一:while True 没有上限

评审员只要一直不说 PASS,这个循环就永远跑下去,每轮烧两次模型调用

必须加:

def loop(task, evaluator_prompt, generator_prompt, max_iterations: int = 3):
...
for _ in range(max_iterations):
...
return result # 到上限了就交付当前最好的版本

建议设得比你以为的小。 实践中超过 3 轮基本不再有质量提升,纯烧钱。

5.2 坑二:定义了三个状态,代码只认一个

提示词里定义了三个值:PASSNEEDS_IMPROVEMENTFAIL。但代码只检查:

if evaluation == "PASS":

NEEDS_IMPROVEMENTFAIL 走的是完全相同的路。 这是实实在在的语义损失:

  • FAIL 应该意味着「方向就错了,推倒重来」→ 应该丢掉之前的版本重新生成
  • NEEDS_IMPROVEMENT 意味着「大方向对,改细节」→ 应该基于当前版本改进

两种情况给生成器的 context 应该完全不同。

另外 == "PASS" 是精确字符串匹配。模型返回 "PASS "(带空格)或 "Pass" 就判定为不通过,白跑一轮。至少要 .strip().upper()

5.3 坑三:memory 累积全部历史版本

context = "\n".join(["Previous attempts:", *[f"- {m}" for m in memory], ...])

第 N 轮的 context 里塞着前 N-1 版完整的代码。三轮下来 prompt 就很可观了。

而且这里有个反直觉的副作用:把所有失败版本都摆在模型面前,会让它锚定在错误方向上。 有时候「忘掉之前所有尝试,只看最新反馈重写」效果反而更好。

这个取舍值得在你自己的任务上实测,不要照抄

六、本篇最重要的一节:为什么它经常空转

现在回到第三节留下的那个问题。上面那份代码,评审员和生成器是同一个模型。这带来一个根本性的问题:

它看不出来的错,换个提示词照样看不出来。

这不是实现问题,是结构问题。学界对此有相当一致的结论:没有外部信号时,LLM 的自我纠错经常改不对,甚至把对的改错。

你会观察到这三种症状:

症状具体表现
来回震荡第 1 轮说「建议加边界检查」,加了;第 2 轮说「代码太啰嗦,建议简化」,删了;第 3 轮又说「建议加边界检查」
永不通过评审员总能找到点什么说 —— 因为「挑刺」这个角色设定本身就在诱导它挑刺
视而不见明明有个真 bug,三轮评审一次都没提到

6.1 解法:把评审那一环接到客观信号上

这是有效 Reflection 和无效 Reflection 的唯一分界线

❌ 无效(空转)✅ 有效
「你觉得这段代码怎么样」「跑测试挂了 3 个,报错如下」
「这篇文案够吸引人吗」「关键词覆盖率 40%,低于阈值 70%」
「这个答案准确吗」「检索到的 5 篇文献里,有 2 处与你的说法矛盾」
「这个 SQL 写得好吗」「EXPLAIN 显示全表扫描,预估行数 200 万」

代码 Agent 是 Reflection 最成功的落地场景,原因就在这里 —— 它天然有编译器和测试套件当评审员。「写代码 → 跑测试 → 看报错 → 改」这个回路里,评审员根本不是模型,是 pytest

所以改造上面那份代码,最值钱的一步不是调 prompt,是把 evaluate 换掉:

def evaluate(content: str, task: str) -> tuple[str, str]:
# ① 先跑客观检查
test_result = run_tests(content)
if test_result.passed:
return "PASS", ""

# ② 只有客观信号说不行时,才让模型解释怎么改
feedback = llm_call(
f"测试失败:{test_result.output}\n"
f"代码:{content}\n"
f"请说明应该怎么改,不要直接给完整代码。"
)
return "NEEDS_IMPROVEMENT", feedback

注意模型的角色变了

模型从「判官」降级成了「翻译官」 —— 它不再负责判断好坏,只负责把客观信号翻译成可执行的修改建议。

这个降级,是 Reflection 能不能上生产的关键。

七、成本收益:什么时候值得

Reflection 是典型的拿钱和时间换质量

成本

成本项具体影响
模型调用每轮至少多两次(评审 + 优化),3 轮就是 6 次
延迟串行,成倍增长,不是并行能救的
开发复杂度要同时调好生成器和评审员两套人格

收益

  • 从「能用」到「好用」的跃迁:功能正确 → 性能高效,逻辑粗糙 → 逻辑严谨
  • 边界情况的覆盖率明显提升

判断标准很简单

对质量要求高、对延迟不敏感 → 用。 在线接口、或者「大致对就行」→ 不用。

关键业务代码、技术报告、决策支持系统适合;聊天回复、简单摘要不适合。

Hello-Agents 4.4.5 专门有一节讲这个成本收益分析,在教程里很少见,值得一读。)

八、常见困惑:Reflection 和强化学习是一回事吗

这是被问得最多的问题,在这里讲清楚。

先给答案:它们是同一个想法在两个不同时间尺度上的实现。

Reflexion 那篇论文的正式标题是 Reflexion: Language Agents with Verbal Reinforcement Learning(用语言表达的强化学习)。它的自我定位就是:用自然语言反馈,代替梯度更新

同一个「测试是否通过」的信号,可以用在两个地方:

信号写到哪叫什么什么时候发生效果存活多久
写进上下文Reflection推理时这个会话
写进权重RLVR(可验证奖励强化学习)训练时永久

所以强化学习不在本专题的六个范式里 —— 它根本不是编排。你换框架、改 prompt 都动不了它。

那「代码 Agent 到底是 Reflection 还是 RL」?两个都是,在不同层上:

想往训练那条线深入,看 Hello-Agents 第十一章《Agentic-RL》,本专题不展开。

九、常见故障与排查

你看到的现象原因怎么修
反馈越来越空泛,质量没变化纯模型自评,在空转接客观信号(第六节)
加了删、删了加,来回改评审标准不稳定记录改动历史,禁止撤销上一轮的修改
永远不给 PASS「挑刺」人格 + 没有明确的通过标准硬性 max_iterations + 在 prompt 里写清 PASS 的条件
评审员直接把代码重写了缺「evaluating only」那句加上(4.2)
模型返回 "Pass" 被判为不通过精确字符串匹配.strip().upper(),或用结构化输出约束枚举
越改越像第一版被历史失败版本锚定试试只传最新反馈,不传全部历史
三轮后 prompt 上万 tokenmemory 全量累积只保留最近一版 + 反馈
成本翻了好几倍但质量没变这个任务不适合 Reflection看第七节的判断标准,该砍就砍

十、全局定位

Reflection 和其他范式不是并列关系,是可叠加的回路。 你可以给一个 Workflow 的某一步加评审,也可以给一个多智能体系统的最终产出加评审。

什么时候不该用

  • 没有客观评价标准 —— 这是硬性前提。说不清什么叫「好」,一定空转
  • 在线接口 —— 串行两次以上模型调用,P99 不可能达标
  • 一次就够好的任务 —— 格式转换、简单摘要,加评审是纯浪费
  • 评审员和生成器都不够强 —— 换个更强的模型比加循环有效得多

十一、小结与检查清单

核心三句话

  1. Reflection 的价值来自角色分离,不是来自「模型会反省」
  2. 评审那一环必须挂客观信号,否则是空转
  3. 模型在有效的 Reflection 里是「翻译官」,不是「判官」

自查清单

  • 评审的依据是客观信号(测试、校验、检索结果),还是模型的主观判断?
  • max_iterations 吗?设的是 3 以内吗?
  • 评审提示词里有「只评审,不要动手解决」吗?
  • 生成器是不是「先写 thoughts 再写 response」?
  • 状态判断有没有做 .strip().upper()
  • FAILNEEDS_IMPROVEMENT 有没有区别对待?
  • 传给生成器的历史,是全部版本还是只有最新一版?实测过哪个好吗?
  • 算过成本吗?多花的钱换来的质量提升值得吗?

参考资料

资料位置协议
本篇拆解的源码patterns/agents/evaluator_optimizer.ipynb,10.8KBMIT
中文教程 + 成本分析Hello-Agents 第四章 4.4CC BY-NC-SA 4.0
论文源码noahshinn/reflexion,3,234★MIT
训练侧延伸Hello-Agents 第十一章 Agentic-RLCC BY-NC-SA 4.0

Shinn N, Cassano F, Gopinath A, et al. Reflexion: Language Agents with Verbal Reinforcement Learning. NeurIPS 2023, 36: 8634-8652.

下一篇:05 · Supervisor 多智能体 —— 一个 Agent 装不下的活,怎么拆给一群。